package net.kong.sns.rest.client.rest;

import net.kong.org.springframework.web.client.RestTemplate;
import net.kong.org.springframework.web.client.RestClientException;
import net.kong.org.springframework.web.client.ResponseExtractor;
import net.kong.org.springframework.web.client.RequestCallback;
import net.kong.org.springframework.http.HttpMethod;
import net.kong.org.springframework.http.MediaType;
import net.kong.org.springframework.http.client.ClientHttpResponse;
import net.kong.org.springframework.http.client.ClientHttpRequest;
import net.kong.org.springframework.http.converter.HttpMessageConverter;
import net.kong.org.springframework.util.LinkedMultiValueMap;
import net.kong.sns.rest.client.method.MethodInvoke;
import net.kong.sns.rest.client.CustomeResponseErrorHandler;
import net.kong.sns.rest.client.SignatureUtils;
import net.kong.sns.rest.client.AuthorizationHeader;
import net.kong.sns.rest.client.ClientException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.io.IOException;

/**
 * @author <a href="mailto:lanhuai@gmail.com">Ning Yubin</a>, <a href="mailto:jolestar@gmail.com">Wang Yuanmin</a>
 * @version $Id: RestClient.java,v 1.11.2.4 2010/01/21 03:32:42 wangyuanming Exp $
 */
public class RestClient {

    private static final Logger LOGGER = LoggerFactory.getLogger(RestClient.class);

    private static final String USER_AGENT = "Kong-SNS-REST-Client_1_1-SNAPSHOT";

    private static final String AUTH_HEADER_NAME = "Authorization";
    
    public static String REST_BASE_URI = "http://kong.net/apps/service";

    public static final String API_VERSION = "1.0";

    private String apiKey;
    private String apiSecret;
    private String sessionKey = null;

    private RestTemplateOnlyPOST restTemplate;

    protected static final ExecutorService executorService = Executors.newFixedThreadPool(15, new ThreadFactory() {
        int i = 0;
        public Thread newThread(Runnable r) {
            return new Thread(r, "kk-sns-rest-client-thread_" + (i++));
        }
    });
    
    public RestClient(String apiKey, String apiSecret) {
        if (apiKey == null) {
            throw new NullPointerException("apiKey");
        }
        if (apiSecret == null) {
            throw new NullPointerException("apiSecret");
        }

        this.apiKey = apiKey;
        this.apiSecret = apiSecret;
        this.restTemplate = new RestTemplateOnlyPOST();

        this.restTemplate.setMessageConverters(new HttpMessageConverter<?>[]{new HttpJsonResponseContentConverter(), new FormHttpMessageConverterUTF8()});
        
        this.restTemplate.setErrorHandler(new CustomeResponseErrorHandler());
    }

    public RestClient(String apiKey, String apiSecret, String sessionKey) {
        this(apiKey, apiSecret);
        this.sessionKey = sessionKey;
    }

    public HttpResponseContent execute(MethodInvoke methodInvoke) {
        return executeInternal(methodInvoke, REST_BASE_URI);
    }

    protected HttpResponseContent executeInternal(MethodInvoke methodInvoke, String restBaseUri) {
        String url = restBaseUri + methodInvoke.getMethodName();
        Map<String, String> params = toParams(methodInvoke);

        AuthorizationHeader authorizationHeader = new AuthorizationHeader(sessionKey);
        authorizationHeader.setApiKey(this.apiKey);
        authorizationHeader.setTimestamp(String.valueOf(System.currentTimeMillis()));
        authorizationHeader.setNonce(UUID.randomUUID().toString());

        String sig = SignatureUtils.generateSignature(params, authorizationHeader, apiSecret, methodInvoke.getHttpMethod(), url);

        authorizationHeader.setSignature(sig);
        restTemplate.setAuthorizationHeader(authorizationHeader);

        try {
            LinkedMultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<String, String>();
            for (Map.Entry<String, String> entry : params.entrySet()) {
                multiValueMap.add(entry.getKey(), entry.getValue());
            }
            return restTemplate.postForObject(url, multiValueMap, HttpResponseContent.class, params);
        } catch (RestClientException e) {
            LOGGER.error("Rest Client Error!", e);
            throw new ClientException("Kong-SNS-REST-Client Error!", e);
        }
    }

    private Map<String, String> toParams(MethodInvoke methodInvoke) {
        Map<String, String> params = new HashMap<String, String>();
        params.putAll(methodInvoke.getParams());
//        params.put(RestParameters.API_KEY, apiKey);
//        if (sessionKey != null) params.put(RestParameters.SESSION_KEY, sessionKey);
        params.put(RestParameters.FORMAT, methodInvoke.getResponseFormat().toString());
        params.put(RestParameters.API_VERSION, API_VERSION);
        return params;
    }

    class RestTemplateOnlyPOST extends RestTemplate {

        private AuthorizationHeader authorizationHeader;

        public AuthorizationHeader getAuthorizationHeader() {
            return authorizationHeader;
        }

        public void setAuthorizationHeader(AuthorizationHeader authorizationHeader) {
            this.authorizationHeader = authorizationHeader;
        }

        public <T> T postForObject(String url, Object request, Class<T> responseType, Map<String, String> urlVariables) throws RestClientException {
            checkForSupportedMessageConverter(responseType);
            return execute(url, HttpMethod.POST, new PostCallback<T>(responseType, request),
                    new HttpMessageConverterExtractor<T>(responseType), urlVariables);
        }

        public <T> T postForObject(String url, Object request, Class<T> responseType, String... urlVariables) throws RestClientException {
            checkForSupportedMessageConverter(responseType);
            return execute(url, HttpMethod.POST, new PostCallback<T>(responseType, request),
                    new HttpMessageConverterExtractor<T>(responseType), urlVariables);
        }

        private void checkForSupportedMessageConverter(Class type) {
            for (HttpMessageConverter<?> entityConverter : getMessageConverters()) {
                if (entityConverter.supports(type)) {
                    return;
                }
            }
            throw new IllegalArgumentException("Could not resolve HttpMessageConverter for [" + type.getName() + "]");
        }
        
        /**
         * Request callback implementation that prepares the request's accept headers.
         */
        private class PostCallback<T> implements RequestCallback {

            private final Class<T> responseType;

            private final Object request;

            private PostCallback(Class<T> responseType, Object request) {
                this.responseType = responseType;
                this.request = request;
            }

            public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
                List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
                for (HttpMessageConverter<?> entityConverter : getSupportedMessageConverters(this.responseType)) {
                    List<MediaType> supportedMediaTypes = entityConverter.getSupportedMediaTypes();
                    for (MediaType supportedMediaType : supportedMediaTypes) {
                        if (supportedMediaType.getCharSet() != null) {
                            supportedMediaType =
                                    new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype());
                        }
                        allSupportedMediaTypes.add(supportedMediaType);
                    }
                }
                Collections.sort(allSupportedMediaTypes);
                httpRequest.getHeaders().setAccept(allSupportedMediaTypes);

                if (request != null) {
                    HttpMessageConverter entityConverter = getSupportedMessageConverters(this.request.getClass()).get(0);
                    entityConverter.write(this.request, httpRequest);
                }
                else {
                    httpRequest.getHeaders().setContentLength(0L);
                }
                

                httpRequest.getHeaders().set(AUTH_HEADER_NAME, authorizationHeader.toString());
                List<String> userAgentList = httpRequest.getHeaders().get("User-Agent");
                String origUserAgent = userAgentList != null && userAgentList.size() > 0 ? userAgentList.get(0) : "";
                String newUserAgent = origUserAgent != null && origUserAgent.trim().length() > 0 ? 
                        USER_AGENT + " (" + httpRequest.getHeaders() + ")" : USER_AGENT;
                httpRequest.getHeaders().set("User-Agent", newUserAgent);
            }
        }

        /**
         * Response extractor that uses the registered {@linkplain HttpMessageConverter entity converters}
         * to convert the response into a type <code>T</code>.
         */
        private class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {

            private final Class<T> responseType;

            private HttpMessageConverterExtractor(Class<T> responseType) {
                this.responseType = responseType;
            }

            public T extractData(ClientHttpResponse response) throws IOException {
                MediaType contentType = response.getHeaders().getContentType();
                if (contentType == null) {
                    throw new RestClientException("Cannot extract response: no Content-Type found");
                }
                for (HttpMessageConverter<T> messageConverter : getSupportedMessageConverters(this.responseType)) {
                    for (MediaType supportedMediaType : messageConverter.getSupportedMediaTypes()) {
                        if (supportedMediaType.includes(contentType)) {
                            return messageConverter.read(this.responseType, response);
                        }
                    }
                }
                throw new RestClientException(
                        "Could not extract response: no suitable HttpMessageConverter found for " + "response type [" +
                                this.responseType.getName() + "] and content type [" + contentType + "]");
            }

        }

    }

    public String getSessionKey() {
        return sessionKey;
    }
    
    public void setSessionKey(String sessionKey) {
        this.sessionKey = sessionKey;
    }

    public String getApiKey() {
        return apiKey;
    }
    
    public String getApiSecret() {
        return apiSecret;
    }

    public abstract class RestParameters {
        public static final String API_KEY = "api_key";
        public static final String API_VERSION = "v";
        public static final String FORMAT = "response_format";
        public static final String METHOD = "method";
        public static final String SESSION_KEY = "session_key";
        public static final String SIGNATURE = "sig";
    }

}
